Fuzzy TOPSIS - Análise de Ações

Bibliotecas

Mostrar/Ocultar Código
import yfinance as yf
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler

Download dos Dados

Mostrar/Ocultar Código
# Tickers da carteira
tickers = ['BRFS3.SA', 'JBSS3.SA', 'BEEF3.SA', 'MRFG3.SA', 'TSN', 'HRL', 'GIS']

# Download dos preços históricos
dados = yf.download(tickers, start="2024-10-01", end="2025-04-25", progress=False)

# Verificação dos dados baixados
if isinstance(dados.columns, pd.MultiIndex):
    downloaded_tickers = dados.columns.levels[1].tolist()
    available_tickers = [ticker for ticker in downloaded_tickers if not dados[('Close', ticker)].isnull().all()]
else:
    if not dados.empty:
        available_tickers = [ticker for ticker in tickers if not dados[ticker].isnull().all()]
    else:
        available_tickers = []

if not available_tickers:
    raise ValueError("❌ Nenhum dado foi baixado. Verifique os tickers e o período.")

print(f"✅ Dados baixados para: {', '.join(available_tickers)}")
C:\Users\kuiav\AppData\Local\Temp\ipykernel_36060\1853906305.py:5: FutureWarning:

YF.download() has changed argument auto_adjust default to True
✅ Dados baixados para: BEEF3.SA, BRFS3.SA, GIS, HRL, JBSS3.SA, MRFG3.SA, TSN

Preparação dos Dados

Mostrar/Ocultar Código
precos = pd.DataFrame()

for ticker in available_tickers:
    if ('Adj Close', ticker) in dados.columns and (not dados[('Adj Close', ticker)].isnull().all()):
        precos[ticker] = dados[('Adj Close', ticker)]
    elif ('Close', ticker) in dados.columns and (not dados[('Close', ticker)].isnull().all()):
        print(f"⚠️ Usando 'Close' para {ticker}, pois 'Adj Close' não está disponível.")
        precos[ticker] = dados[('Close', ticker)]
    else:
        print(f"❌ Dados insuficientes para {ticker}. Ignorando.")

if precos.empty:
    raise ValueError("❌ Nenhuma coluna válida encontrada para análise.")

precos = precos.dropna()

if precos.empty:
    raise ValueError("❌ Dados insuficientes após remoção de valores nulos.")
⚠️ Usando 'Close' para BEEF3.SA, pois 'Adj Close' não está disponível.
⚠️ Usando 'Close' para BRFS3.SA, pois 'Adj Close' não está disponível.
⚠️ Usando 'Close' para GIS, pois 'Adj Close' não está disponível.
⚠️ Usando 'Close' para HRL, pois 'Adj Close' não está disponível.
⚠️ Usando 'Close' para JBSS3.SA, pois 'Adj Close' não está disponível.
⚠️ Usando 'Close' para MRFG3.SA, pois 'Adj Close' não está disponível.
⚠️ Usando 'Close' para TSN, pois 'Adj Close' não está disponível.

Cálculo de Retornos

Mostrar/Ocultar Código
retornos_diarios = precos.pct_change().dropna()

if retornos_diarios.empty:
    raise ValueError("❌ Retornos diários insuficientes.")

retorno_medio_anual = (retornos_diarios.mean() * 252) * 100
risco_anual = (retornos_diarios.std() * np.sqrt(252)) * 100

df = pd.DataFrame({
    'Ticker': retorno_medio_anual.index,
    'Retorno Esperado (%)': retorno_medio_anual.values,
    'Risco (%)': risco_anual.values
})

df
Ticker Retorno Esperado (%) Risco (%)
0 BEEF3.SA 19.952812 52.777333
1 BRFS3.SA -5.995730 38.618763
2 GIS -44.667148 24.262770
3 HRL -1.237448 21.940254
4 JBSS3.SA 82.872586 41.321218
5 MRFG3.SA 121.350365 47.075397
6 TSN 10.791442 22.756918

Normalização e Pesos (Fuzzy TOPSIS)

Mostrar/Ocultar Código
df_normalized = df.copy()

scaler_ret = MinMaxScaler()
df_normalized['Retorno Normalizado'] = scaler_ret.fit_transform(df[['Retorno Esperado (%)']])

scaler_risk = MinMaxScaler()
df_normalized['Risco Normalizado'] = 1 - scaler_risk.fit_transform(df[['Risco (%)']])

peso_retorno = 0.6
peso_risco = 0.4

df_normalized['Retorno Ponderado'] = df_normalized['Retorno Normalizado'] * peso_retorno
df_normalized['Risco Ponderado'] = df_normalized['Risco Normalizado'] * peso_risco

Cálculo do TOPSIS

Mostrar/Ocultar Código
ideal_positivo = [
    df_normalized['Retorno Ponderado'].max(),
    df_normalized['Risco Ponderado'].max()
]

ideal_negativo = [
    df_normalized['Retorno Ponderado'].min(),
    df_normalized['Risco Ponderado'].min()
]

distancia_positiva = np.sqrt(
    (df_normalized['Retorno Ponderado'] - ideal_positivo[0])**2 +
    (df_normalized['Risco Ponderado'] - ideal_positivo[1])**2
)

distancia_negativa = np.sqrt(
    (df_normalized['Retorno Ponderado'] - ideal_negativo[0])**2 +
    (df_normalized['Risco Ponderado'] - ideal_negativo[1])**2
)

df_normalized['Índice Similaridade'] = distancia_negativa / (distancia_positiva + distancia_negativa)
df_normalized['Rank'] = df_normalized['Índice Similaridade'].rank(ascending=False)

Resultado Final

Mostrar/Ocultar Código
resultado = df_normalized[['Ticker', 'Retorno Esperado (%)', 'Risco (%)', 'Índice Similaridade', 'Rank']].sort_values(by='Rank')
resultado
Ticker Retorno Esperado (%) Risco (%) Índice Similaridade Rank
5 MRFG3.SA 121.350365 47.075397 0.649640 1.0
4 JBSS3.SA 82.872586 41.321218 0.627660 2.0
6 TSN 10.791442 22.756918 0.522833 3.0
3 HRL -1.237448 21.940254 0.492352 4.0
2 GIS -44.667148 24.262770 0.381066 5.0
1 BRFS3.SA -5.995730 38.618763 0.312154 6.0
0 BEEF3.SA 19.952812 52.777333 0.300945 7.0